package dmxGroupControl;

import dmxGroupControl.listeners.Listener;
import dmxGroupControl.listeners.MIDIListener;
import dmxGroupControl.listeners.OSCListener;
import dmxGroupControl.solvers.Solver;
import dmxGroupControl.transmitters.*;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.input.SAXBuilder;
import org.jdom2.input.sax.XMLReaders;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;


public class LightController
{
    public static final int STROBE_TIME = 30; // strobe at about 40hz
    public static boolean strobeOn = true;
    public static Timer strobeTimer;
    private static String fixturesFileURL = "data/fixtures.xml";
    private static String listenersFileURL = "data/listeners.xml";
    private static String transmittersFileURL = "data/transmitters.xml";
    private static String colorWheelsFileURL = "data/colorwheels.xml";
    private static LightController controller;
    private String universesFileURL;
    private ArrayList<Universe> universes;
    private HashMap<String, Listener> listeners;
    private boolean solversEnabled;
    private boolean updatesEnabled = false;

    public LightController(String universesFileURL)
    {
        this.universesFileURL = universesFileURL;

        ReadXML(universesFileURL);
        ReadListeners(listenersFileURL);
        ReadTransmitters(transmittersFileURL);

        TimerTask task = new TimerTask()
        {
            @Override
            public void run()
            {
                strobeOn = !strobeOn;
            }
        };

        strobeTimer = new Timer(true);
        strobeTimer.scheduleAtFixedRate(task, 0, STROBE_TIME);

        solversEnabled = true;

        controller = this;
        updatesEnabled = true;
    }

    public static LightController GetInstance()
    {
        return controller;
    }

    public static void Initialize(String universesXML)
    {
        controller = new LightController(universesXML);
    }

    public static boolean AreUpdatesEnabled()
    {
        return (GetInstance() != null &&
                controller.updatesEnabled);
    }

    public static Element GetFixtureDefinition(String name)
    {
        SAXBuilder builder = new SAXBuilder(XMLReaders.XSDVALIDATING);
        try
        {
            Document doc = builder.build(fixturesFileURL);
            Element root = doc.getRootElement();
            return GetElementByID(root, name);
        }
        catch (JDOMException | IOException e)
        {

            e.printStackTrace();
        }
        return null;
    }

    private static Element GetElementByID(@NotNull Element root, @NotNull String id)
    {
        if (id == null)
        {
            return null;
        }
        for (Element element : root.getChildren())
        {
            if (element.getAttributeValue("id").compareTo(id) == 0)
            {
                return element;
            }
        }
        return null;
    }

    public ArrayList<Universe> GetUniverses()
    {
        return universes;
    }

    public void close()
    {
        strobeTimer.cancel();
        for (Listener listener : listeners.values())
        {
            listener.close();
        }
        for (Universe universe : universes)
        {
            universe.close();
        }
    }

    private void ReadListeners(String listenersFileURL)
    {
        File listenersFile = new File(listenersFileURL);
        SAXBuilder builder = new SAXBuilder(XMLReaders.XSDVALIDATING);

        Document listenersDoc;
        try
        {
            listenersDoc = builder.build(listenersFile);
        }
        catch (JDOMException | IOException e)
        {
            e.printStackTrace();
            return;
        }

        Element listenersRoot = listenersDoc.getRootElement();
        listeners = new HashMap<>();

        for (Element listenerElement : listenersRoot.getChildren())
        {
            Listener listener;
            String id = listenerElement.getAttributeValue("id");
            String type = listenerElement.getName();
            boolean enabled = Boolean.parseBoolean(GetOptionalAttribute(listenerElement, "enabled", "true"));
            switch (type)
            {
                case "oscListener":
                    int listenPort = Integer.parseInt(listenerElement.getAttributeValue("port"));
                    listener = new OSCListener(id, listenPort);
                    break;
                case "midiListener":
                    String deviceName = listenerElement.getAttributeValue("device");
                    listener = new MIDIListener(id, deviceName);
                    break;
                default:
                    continue;
            }
            listener.SetEnabled(enabled);
            listeners.put(id, listener);

            Element listenerMapElement = listenerElement.getChild("listenerMap");
            String universeName = listenerMapElement.getAttributeValue("id");
            int startIndex = Integer.parseInt(listenerMapElement.getAttributeValue("startIndex"));
            Universe universe = universes.stream().filter(uni -> uni.GetName().equals(universeName)).findAny().get();
            listener.ParseMappings(universe, startIndex, listenerMapElement);
        }
    }

    private void ReadTransmitters(String transmittersFileURL)
    {
        File transmittersFile = new File(transmittersFileURL);
        SAXBuilder builder = new SAXBuilder(XMLReaders.XSDVALIDATING);

        Document transmittersDoc;
        try
        {
            transmittersDoc = builder.build(transmittersFile);
        }
        catch (JDOMException | IOException e)
        {
            e.printStackTrace();
            return;
        }

        Element transmittersRoot = transmittersDoc.getRootElement();

        String universeName = transmittersRoot.getAttributeValue("id");
        Universe universe = universes.stream().filter(uni -> uni.GetName().equals(universeName)).findAny().get();

        for (Element transmitterElement : transmittersRoot.getChildren())
        {
            universe.GetTransmitters().add(
                Transmitter.CreateTransmitter(transmitterElement)
            );
        }
    }

    private String GetOptionalAttribute(Element element, String attribute, String defaultValue)
    {
        if (element.getAttribute(attribute) != null)
        {
            return element.getAttributeValue(attribute);
        }
        return defaultValue;
    }

    private void ReadXML(String universesFileURL)
    {
        // get the settings from the xml file
        File universesFile = new File(universesFileURL);
        SAXBuilder builder = new SAXBuilder(XMLReaders.XSDVALIDATING);

        Document universesDoc;
        try
        {
            universesDoc = builder.build(universesFile);
        }
        catch (JDOMException | IOException e)
        {
            System.err.println("Unable to load universes file " + universesFileURL);
            System.err.println(e.getMessage());
            return;
        }

        Element universesRoot = universesDoc.getRootElement();

        universes = new ArrayList<>();

        List<Element> universeElements = universesRoot.getChildren("universe");
        for (Element universeElement : universeElements)
        {
            String universeName = universeElement.getAttributeValue("name");

            Universe universe = new Universe(universeName);
            universes.add(universe);

            for (Element groupElement : universeElement.getChild("groups").getChildren("group"))
            {
                String name = groupElement.getAttributeValue("name");

                Group group = new Group(name);

                groupElement
                    .getChildren("dmxFixture")
                    .forEach(fixtureElement -> group.GetFixtures().add(Fixture.CreateFixture(fixtureElement)));

                groupElement
                    .getChildren("pixelFixture")
                    .forEach(fixtureElement -> group.GetFixtures().add(Fixture.CreateFixture(fixtureElement)));

                groupElement
                    .getChildren("solver")
                    .forEach(solverElement -> group.GetSolvers().add(Solver.CreateSolver(solverElement, group)));

                universe.GetGroups().add(group);
            }
        }

    }

    public void SetSolversEnabled(boolean value)
    {
        solversEnabled = value;
    }

    public boolean AreSolversEnabled()
    {
        return solversEnabled;
    }

    public void DeleteFixture(Fixture fixture)
    {
        for (Universe universe : universes)
        {
            for (Group group : universe.GetGroups())
            {
                if (group.GetFixtures().contains(fixture))
                {
                    group.GetFixtures().remove(fixture);
                    return;
                }
            }
        }
    }

    @Override
    public String toString()
    {
        return "Lighting Controller";
    }

    public Universe FindUniverse(String universeName)
    {
        for (Universe universe : universes)
        {
            if (universe.toString().compareTo(universeName) == 0)
            {
                return universe;
            }
        }
        return null;
    }

    public Listener FindListener(String listenerName)
    {
        for (Listener listener : listeners.values())
        {
            if (listener.toString().compareTo(listenerName) == 0)
            {
                return listener;
            }
        }
        return null;
    }

    public ArrayList<String> GetFixtureNames()
    {
        ArrayList<String> fixtures = new ArrayList<>();
        SAXBuilder builder = new SAXBuilder(XMLReaders.XSDVALIDATING);
        try
        {
            Document doc = builder.build(fixturesFileURL);
            Element root = doc.getRootElement();
            for (Element element : root.getChildren("fixture"))
            {
                String name = element.getAttributeValue("id");
                fixtures.add(name);
            }
        }
        catch (JDOMException | IOException e)
        {
            System.err.println("Could not read fixtures file");
            System.err.println(e.getMessage());
            e.printStackTrace();
        }

        return fixtures;
    }

    public void SaveFixtureDefinition(Fixture fixture)
    {
        int channelStart = 1;
        if (fixture instanceof DMXFixture)
        {
            channelStart = ((DMXFixture) fixture).GetChannelStart();
        }
        SAXBuilder builder = new SAXBuilder();
        try
        {
            Document doc = builder.build(fixturesFileURL);
            Element root = doc.getRootElement();
            Element fixtureElement = GetElementByID(root, fixture.GetName());
            if (fixtureElement != null)
            {
                fixtureElement.getChildren().clear();
            }
            else
            {
                fixtureElement = new Element("fixture");
                fixtureElement.setAttribute("id", fixture.GetName());
                fixtureElement.setAttribute("type", fixture.GetFixtureType().GetFriendlyName());
                root.getChildren().add(fixtureElement);
            }

            for (Channel channel : fixture.GetChannels())
            {
                Element channelElement = new Element("channel");
                channelElement.setAttribute("name", channel.GetName());

                channelElement.setAttribute("dmx", channel.GetDMXChannel() - channelStart + 1 + "");
                channelElement.setAttribute("min", channel.GetMin() + "");
                channelElement.setAttribute("max", channel.GetMax() + "");
                fixtureElement.getChildren().add(channelElement);
            }
            root.sortChildren(
                    (first, second) -> first.getAttributeValue("id").compareToIgnoreCase(second.getAttributeValue("id"))
                             );

            XMLOutputter xmlOutputter = new XMLOutputter(Format.getPrettyFormat());
            xmlOutputter.output(doc, new FileOutputStream(fixturesFileURL));

        }
        catch (JDOMException | IOException e)
        {
            e.printStackTrace();
        }
    }

    public void SaveUniverses()
    {
        Document doc = new Document();
        Element root = new Element("venue");
        Namespace xsi = Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
        root.setAttribute("noNamespaceSchemaLocation", "venue.xsd", xsi);
        doc.setRootElement(root);

        for (Universe universe : universes)
        {
            Element universeElement = new Element("universe");
            universeElement.setAttribute("name", universe.GetName());
            root.getChildren().add(universeElement);

            Element groupsElement = new Element("groups");
            universeElement.getChildren().add(groupsElement);

            for (Group group : universe.GetGroups())
            {
                Element groupElement = new Element("group");
                groupElement.setAttribute("name", group.GetName());
                groupsElement.getChildren().add(groupElement);
                for (Fixture fixture : group.GetFixtures())
                {
                    Element fixtureElement;
                    if (fixture instanceof DMXFixture)
                    {
                        fixtureElement = new Element("dmxFixture");
                        fixtureElement.setAttribute("startChannel", ((DMXFixture) fixture).GetChannelStart() + "");
                    }
                    else if (fixture instanceof PixelFixture)
                    {
                        fixtureElement = new Element("pixelFixture");
                        fixtureElement.setAttribute("xCount", ((PixelFixture) fixture).GetXDimensions() + "");
                        fixtureElement.setAttribute("yCount", ((PixelFixture) fixture).GetYDimensions() + "");
                    }
                    else
                    {
                        continue;
                    }
                    fixtureElement.setAttribute("type", fixture.GetName());
                    groupElement.getChildren().add(fixtureElement);
                    for (Solver solver : fixture.GetSolvers())
                    {
                        Element solverElement = new Element("solver");
                        solverElement.setAttribute("type", solver.GetType());
                        solverElement.setAttribute("enabled", solver.GetEnabled() + "");
                        solverElement.getAttributes().addAll(solver.GetAttributes());
                        fixtureElement.getChildren().add(solverElement);
                    }
                }

                for (Solver solver : group.GetSolvers())
                {
                    Element solverElement = new Element("solver");
                    solverElement.setAttribute("type", solver.GetType());
                    solverElement.setAttribute("enabled", solver.GetEnabled() + "");
                    solverElement.getAttributes().addAll(solver.GetAttributes());
                    groupElement.getChildren().add(solverElement);
                }
            }
        }

        XMLOutputter xmlOutputter = new XMLOutputter(Format.getPrettyFormat());
        try
        {
            xmlOutputter.output(doc, new FileOutputStream(universesFileURL));
        }
        catch (IOException e)
        {
            System.err.println("Unable to write to venue file " + universesFileURL);
            e.printStackTrace();
        }
    }

    public void DeleteFixtureDefinition(String fixtureName)
    {
        SAXBuilder builder = new SAXBuilder();
        try
        {
            Document doc = builder.build(fixturesFileURL);
            Element root = doc.getRootElement();
            Element fixtureElement = GetElementByID(root, fixtureName);

            if (fixtureElement != null)
            {
                root.getChildren().remove(fixtureElement);
            }

            XMLOutputter xmlOutputter = new XMLOutputter(Format.getPrettyFormat());
            xmlOutputter.output(doc, new FileOutputStream(fixturesFileURL));

        }
        catch (JDOMException | IOException e)
        {
            e.printStackTrace();
        }
    }

    public void SetUpdatesEnabled(boolean value)
    {
        updatesEnabled = value;
    }

    public Listener GetListener(String id)
    {
        return listeners.get(id);
    }

    public Collection<Listener> GetListeners()
    {
        return listeners.values();
    }

    public Collection<String> GetListenerIDs()
    {
        return listeners.keySet();
    }

    public static Element GetColorWheelDefinition(String fixtureType)
    {
        SAXBuilder builder = new SAXBuilder(XMLReaders.XSDVALIDATING);
        try
        {
            Document doc = builder.build(colorWheelsFileURL);
            Element root = doc.getRootElement();
            return GetElementByID(root, fixtureType);
        }
        catch (JDOMException | IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}
